///-----------------------------------------------------------------------------
/// Filename: Model.cs
///
/// Description: 
/// Template for an XML based model used in an MVC framework.
/// Allows an XML approach for passing the model between the controller
/// and the view
///
/// History:
/// 08 Mar 2004 Aaron Clauson	Created.
///								
/// License:
/// Public Domain.
///-----------------------------------------------------------------------------

using System;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;
using MaverickLite.Config;
using log4net;

#if UNITTEST
using NUnit.Framework;
#endif

namespace MaverickLite.Model
{
	/// <summary>
	/// Model that allows a combination of xml fragments or documents to
	/// be aggregated into a single model.
	/// </summary>
	public class XmlAggregatorModel
	{
		public const string MODEL_ROOT_NODE_TAG = "page";

		public Hashtable SpecialParameters = new Hashtable();	// Collection of special parameters that are converted within the model, e.g. ${path} etc.
		
		private ILog logger = MavAppState.logger;

		private XmlDocument m_modelDom = new XmlDocument();		// The concrete model.

		// Need to attempt to add current model into the model dom.
		// For simplicity will only support XmlDocument, XmlElement,
		// XmlNode, XPathNaviagtor, String and if no parameter, in which case an
		// empty model will be created.

		public XmlAggregatorModel()
		{
			m_modelDom.LoadXml("<" + MODEL_ROOT_NODE_TAG + "/>");
		}

		public XmlAggregatorModel(Hashtable specialParameters)
		{
			if(specialParameters != null)
			{
				SpecialParameters = specialParameters;
			}

			m_modelDom.LoadXml("<" + MODEL_ROOT_NODE_TAG + "/>");
		}

	
		public XmlAggregatorModel(XmlDocument currentModel)
		{
			string inRootNodeName = currentModel.DocumentElement.LocalName;
			
			// Check that root node is as expected.
			if(inRootNodeName != MODEL_ROOT_NODE_TAG)
			{
				throw new ApplicationException("The input model did not have the correct " +
					 "root node.  It was " + inRootNodeName + " and should have been " + MODEL_ROOT_NODE_TAG);
			}

			XmlNode rootNode = m_modelDom.ImportNode(currentModel.DocumentElement, true);
			m_modelDom.AppendChild(rootNode);
		}

		public XmlAggregatorModel(XmlElement currentModel)
		{
			string inRootNodeName = currentModel.LocalName;
			
			// Check that root node is as expected.
			if(inRootNodeName != MODEL_ROOT_NODE_TAG)
			{
				throw new ApplicationException("The input model did not have the correct " +
					"root node.  It was " + inRootNodeName + " and should have been " + MODEL_ROOT_NODE_TAG);
			}

			m_modelDom.LoadXml(currentModel.OuterXml);
		}

		public XmlAggregatorModel(XmlNode currentModel)
		{
			string inRootNodeName = currentModel.LocalName;
			
			// Check that root node is as expected.
			if(inRootNodeName != MODEL_ROOT_NODE_TAG)
			{
				throw new ApplicationException("The input model did not have the correct " +
					"root node.  It was " + inRootNodeName + " and should have been " + MODEL_ROOT_NODE_TAG);
			}

			m_modelDom.LoadXml(currentModel.OuterXml);
		}

		public XmlAggregatorModel(XPathNavigator currentModel)
		{
			// Move the navigator to the first element and get its name.
			string inRootNodeName = currentModel.LocalName;

			while(inRootNodeName == null || inRootNodeName.Trim() == String.Empty)
			{
				if(!currentModel.MoveToNext())
				{
					break;
				}

				inRootNodeName = currentModel.LocalName;
			}
			
			// Check that root node is as expected.
			if(inRootNodeName != MODEL_ROOT_NODE_TAG)
			{
				throw new ApplicationException("The input model did not have the correct " +
					"root node.  It was " + inRootNodeName + " and should have been " + MODEL_ROOT_NODE_TAG);
			}

			XmlNode node = ((IHasXmlNode)currentModel).GetNode();
			m_modelDom.LoadXml(node.OuterXml);
		}

		public XmlAggregatorModel(string currentModel)
		{
			if(currentModel == null || currentModel.Trim() == String.Empty)
			{
				currentModel = "<" + MODEL_ROOT_NODE_TAG + "/>";
			}
			else
			{
				// Extract the name of the first xml node from the string.
				string inRootNodeName = Regex.Match(currentModel, @"^\<(?<rootNode>\S+?)(\/|\>).*").Result("${rootNode}");;
			
				// Check that root node is as expected.
				if(inRootNodeName != MODEL_ROOT_NODE_TAG)
				{
					throw new ApplicationException("The input model did not have the correct " +
						"root node.  It was " + inRootNodeName + " and should have been " + MODEL_ROOT_NODE_TAG);
				}
			}

			m_modelDom.LoadXml(currentModel);
		}

		/// <summary>
		/// Returns a read only copy of the model to the caller.
		/// </summary>
		/// <returns>A string representation of the model xml dom.</returns>
		public XmlDocument GetModel()
		{
			if(m_modelDom.DocumentElement != null)
			{
				XmlDocument parsedDom = ParseSpecialParameters(m_modelDom, SpecialParameters);
				
				return parsedDom;
			}
			else
			{
				return null;
			}
		}

		public XmlDocument ParseSpecialParameters(XmlDocument modelDom, Hashtable specialParameters)
		{
			if(specialParameters == null || specialParameters.Count == 0)
			{
				return modelDom;
			}
			else
			{
				string model = modelDom.OuterXml;

				foreach(DictionaryEntry specialParameter in specialParameters)
				{
					if(specialParameter.Value != null)
					{
						model = Regex.Replace(model, Regex.Escape((string)specialParameter.Key), (string)specialParameter.Value);
					}
				}

				XmlDocument parsedDom = new XmlDocument();
				parsedDom.LoadXml(model);

				return parsedDom;
			}
		}

		/// <summary>
		/// Attempts to aggregate the xml fragment into the model.
		/// </summary>
		/// <param name="xmlFragment">The fragment to insert into the model. The string must 
		/// be valid conforming xml.</param>
		/// <param name="overwrite">Whether the child nodes being inserted should overwrite
		/// exisitng ones.</param>
		/// <returns>True if successfull.</returns>
		public bool AddToModel(string xmlFragment, bool overwrite)
		{	
			try
			{
				if(xmlFragment == null)
				{
					return false;
				}
				
				XmlDocument fragmentDom = new XmlDocument();
				fragmentDom.LoadXml(xmlFragment);

				AddToModel(fragmentDom.DocumentElement, overwrite);

				return true;
			}
			catch(Exception excp)
			{
				logger.Error("Exception XmlAggregatorModel: " + excp.Message);
				logger.Error("XML: " + xmlFragment);
				return false;
			}
		}


		/// <summary>
		/// Attempts to add a new node to the model when the new node should be added as an 
		/// additional node and not overwrite or be combined with any exisitng nodes.
		/// </summary>
		/// <param name="xmlFragment">The fragment to insert into the model. The string must 
		/// be valid conforming xml.</param>
		/// <param name="overwrite">Whether the child nodes being inserted should overwrite
		/// exisitng ones.</param>
		/// <returns>True if successfull.</returns>
		public bool AddAdditionalToModel(string xmlFragment, bool overwrite)
		{	
			try
			{
				if(xmlFragment == null)
				{
					return false;
				}
				
				XmlDocument fragmentDom = new XmlDocument();
				fragmentDom.LoadXml(xmlFragment);

				if(fragmentDom == null)
				{
					return false;
				}
			
				string inRootNodeName = fragmentDom.DocumentElement.LocalName;
			
				// Check that root node of the model is as expected.
				if(inRootNodeName != MODEL_ROOT_NODE_TAG)
				{
					throw new ApplicationException("An attempt was made to insert an element with an invalid " +
						"root node.  It was " + inRootNodeName + " and should have been " + MODEL_ROOT_NODE_TAG);
				}

				XmlNode newChildNode = this.m_modelDom.ImportNode(fragmentDom.DocumentElement.FirstChild, true);
				m_modelDom.DocumentElement.AppendChild(newChildNode);

				return true;
			}
			catch(Exception excp)
			{
				logger.Error("Exception XmlAggregatorModel: " + excp.Message);
				logger.Error("XML: " + xmlFragment);
				return false;
			}
		}

		/// <summary>
		/// Add some information to the model.
		/// </summary>
		/// <param name="modelNode">Element to add into the model.</param>
		/// <param name="overwrite">Determines whether the nodes being inserted should
		/// overwrite existing nodes or be added as siblings.</param>
		/// <returns>True if the sub model was successfully added otherwise false.</returns>
		public bool AddToModel(XmlElement modelNode, bool overwrite)
		{
			if(modelNode == null)
			{
				return false;
			}
			
			string inRootNodeName = modelNode.LocalName;
			
			// Check that root node of the model is as expected.
			if(inRootNodeName != MODEL_ROOT_NODE_TAG)
			{
				throw new ApplicationException("An attempt was made to insert an element with an invalid " +
					"root node.  It was " + inRootNodeName + " and should have been " + MODEL_ROOT_NODE_TAG);
			}

			foreach(XmlNode childNode in modelNode.ChildNodes)
			{
				string insertNodeName = childNode.LocalName;

				// Attempt to get any nodes with the same name that are already in the model.
				XmlNode existingNode = m_modelDom.DocumentElement.SelectSingleNode(insertNodeName);

				if(existingNode == null)
				{
					XmlNode newChildNode = this.m_modelDom.ImportNode(childNode, true);
					m_modelDom.DocumentElement.AppendChild(newChildNode);
				}
				else if(overwrite)
				{
					// Overwrite the existing node.
					XmlNode newChildNode = this.m_modelDom.ImportNode(childNode, true);
					m_modelDom.DocumentElement.ReplaceChild(newChildNode, existingNode);
				}
				else
				{	
					// Add the grand children of the insert node as children of the existing node.
					foreach(XmlNode grandChildNode in childNode.ChildNodes)
					{	
						XmlNode newGrandChildNode = this.m_modelDom.ImportNode(grandChildNode, true);
						existingNode.AppendChild(newGrandChildNode);
					}
				}
			}
			
			return true;
		}


		/// <summary>
		/// Attempts to load an xml document from the physical file paths in the path nodes
		/// and insert into the aggregated model.
		/// </summary>
		/// <param name="pathNodes">List of relative paths, typically from a controller configuration file.</param>
		/// <param name="applicationPath">The physical path to the relative path directory.</param>
		/// <returns>True if at least one fragment was added to the model, otherwise false.</returns>
		public bool AddToModel(XmlNodeList pathNodes, string applicationPath)
		{
			if(pathNodes == null || pathNodes.Count == 0)
			{
				return false;
			}

			ArrayList paths = new ArrayList();

			foreach(XmlNode pathNode in pathNodes)
			{
				// Attempt to load the dom specified and add it to the controller dom.
				paths.Add(new DictionaryEntry(applicationPath + pathNode.Attributes["value"].Value, false));
			}

			return AddToModel(paths, null);
		}


		/// <summary>
		/// Attempts to load an xml document from the physical file paths in the path nodes
		/// and insert into the aggregated model.
		/// </summary>
		/// <param name="paths">List of relative paths, typically from a controller configuration file
		/// and whether they are overwrite or not.</param>
		/// <param name="applicationPath">The physical path to the relative path directory.</param>
		/// <returns>True if at least one fragment was added to the model, otherwise false.</returns>
		public bool AddToModel(ArrayList paths, string applicationPath)
		{
			bool result = false;
			
			try
			{
				if(paths == null || paths.Count == 0)
				{
					return false;
				}

				foreach(DictionaryEntry modelItem in paths)
				{
					string modelPath = (string)modelItem.Key;

					if(modelPath != null && !Regex.Match(modelPath, @"^\w:\\").Success)
					{
						// Path is under the web site directory.
						modelPath = applicationPath + (string)modelItem.Key;
					}
				
					// Attempt to load the dom specified and add it to the aggregated dom.
					if(File.Exists(modelPath))
					{
						try
						{
							XmlDocument modelStep = new XmlDocument();
							modelStep.Load(modelPath);
							AddToModel(modelStep.DocumentElement, (bool)modelItem.Value);

							result = true;
						}
						catch(Exception invalidXmlExcp)
						{
							logger.Error("AddToModel was expecting a valid xml file but was not given one, " + modelPath + ". " + invalidXmlExcp.Message);
						}
					}
					else
					{
						logger.Info("AddToModel was requested to add non-existent file, " + modelPath + ".");
					}
				}

				return result;
			}
			catch(Exception excp)
			{
				logger.Error("Exception AddToModel: " + excp.Message);
				return result;
			}
		}

		/// <summary>
		/// Creates a copy of the current object.
		/// </summary>
		/// <returns>Copy of current object.</returns>
		public XmlAggregatorModel Clone()
		{
			XmlAggregatorModel clonedModel = new XmlAggregatorModel();
			clonedModel.m_modelDom.LoadXml(this.m_modelDom.OuterXml);

			return clonedModel;
		}


		#region Unit tests.

		#if UNITTEST
		
		[TestFixture]
		public class XmlAggregatorModelUnitTest
		{
			XmlDocument m_inModel = new XmlDocument();
			
			[SetUp]
			public void Init()
			{
				m_inModel.LoadXml(
					"<page>" +
					" <title>Page Title</title>" +
					" <contents>" +
					"  <div>" +
					"   Hello World!" +
					"  </div>" +
					" </contents>" +
					"</page>");
			}
			
			/// <summary>
			/// Test the constructor with a valid xml dom model input.
			/// </summary>
			[Test]
			public void ConstructorDom()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel);
				
				Assertion.Assert("Input model doesn't match document model.\n" 
					+ "Original = " + m_inModel.OuterXml+ "\n" 
					+ "Document Model = " +	model.m_modelDom.OuterXml, 
					model.m_modelDom.OuterXml == m_inModel.OuterXml);
			}

			/// <summary>
			/// Test the constructor with a valid xml element input.
			/// </summary>
			[Test]
			public void ConstructorElement()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel.DocumentElement);
				
				Assertion.Assert("Input model doesn't match document model.\n" 
					+ "Original = " + m_inModel.OuterXml+ "\n" 
					+ "Document Model = " +	model.m_modelDom.OuterXml, 
					model.m_modelDom.OuterXml == m_inModel.OuterXml);
			}

			/// <summary>
			/// Test the constructor with a valid xml node input.
			/// </summary>
			[Test]
			public void ConstructorNode()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel.SelectSingleNode("/page"));
				
				Assertion.Assert("Input model doesn't match document model.\n" 
					+ "Original = " + m_inModel.OuterXml+ "\n" 
					+ "Document Model = " +	model.m_modelDom.OuterXml, 
					model.m_modelDom.OuterXml == m_inModel.OuterXml);
			}

			/// <summary>
			/// Test the constructor with a valid xml path navigator input.
			/// </summary>
			[Test]
			public void ConstructorNavigator()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel.DocumentElement.CreateNavigator());
				
				Assertion.Assert("Input model doesn't match document model.\n" 
					+ "Original = " + m_inModel.OuterXml+ "\n" 
					+ "Document Model = " +	model.m_modelDom.OuterXml, 
					model.m_modelDom.OuterXml == m_inModel.OuterXml);
			}

			/// <summary>
			/// Test the constructor with a valid xml string input.
			/// </summary>
			[Test]
			public void ConstructorString()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel.OuterXml);
				
				Assertion.Assert("Input model doesn't match document model.\n" 
					+ "Original = " + m_inModel.OuterXml+ "\n" 
					+ "Document Model = " +	model.m_modelDom.OuterXml, 
					model.m_modelDom.OuterXml == m_inModel.OuterXml);
			}

			/// <summary>
			/// Test the constructor with an invalid xml dom model input.
			/// </summary>
			[Test]
			[ExpectedException(typeof(ApplicationException))]
			public void ConstructorInvalidDom()
			{
				XmlAggregatorModel model = new XmlAggregatorModel("<notpage/>");
				
				Assertion.Assert("Input model doesn't match document model.\n" 
					+ "Original = " + m_inModel.OuterXml+ "\n" 
					+ "Document Model = " +	model.m_modelDom.OuterXml, 
					model.m_modelDom.OuterXml == m_inModel.OuterXml);
			}

			[Test]
			public void AddSimpleNode()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel);

				XmlDocument insertModel = new XmlDocument();
				insertModel.LoadXml(
					"<page>" +
					" <contents>" +
					"  <footer>Powered by Maverick.Net</footer>" +
					" </contents>" +
					"</page>");

				bool result = model.AddToModel(insertModel.DocumentElement, false);

				Assertion.Assert("Insert returned false.", result);
				
				XmlDocument resultModel = new XmlDocument();
				
				resultModel.LoadXml(
					"<page>" +
					" <title>Page Title</title>" +
					" <contents>" +
					"  <div>" +
					"   Hello World!" +
					"  </div>" +
					"  <footer>Powered by Maverick.Net</footer>" +
					" </contents>" +
					"</page>");
				
				Assertion.Assert("New model was not correctly inserted.  Model is " +
					model.m_modelDom.OuterXml + "\nShould have been " + resultModel.OuterXml, 
					model.m_modelDom.OuterXml == resultModel.OuterXml);
			}

			[Test]
			public void AddSimpleNodeOverwrite()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel);

				XmlDocument insertModel = new XmlDocument();
				insertModel.LoadXml(
					"<page>" +
					" <contents>" +
					"  <footer>Powered by Maverick.Net</footer>" +
					" </contents>" +
					"</page>");

				bool result = model.AddToModel(insertModel.DocumentElement, true);

				Assertion.Assert("Insert returned false.", result);
				
				XmlDocument resultModel = new XmlDocument();
				
				resultModel.LoadXml(
					"<page>" +
					" <title>Page Title</title>" +
					" <contents>" +
					"  <footer>Powered by Maverick.Net</footer>" +
					" </contents>" +
					"</page>");
				
				Assertion.Assert("New model was not correctly inserted.  Model is " +
					model.m_modelDom.OuterXml + "\nShould have been " + resultModel.OuterXml, 
					model.m_modelDom.OuterXml == resultModel.OuterXml);
			}

			[Test]
			public void AddSimpleNodeNew()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel);

				XmlDocument insertModel = new XmlDocument();
				insertModel.LoadXml(
					"<page>" +
					" <identity>" +
					"  aaron" +
					" </identity>" +
					"</page>");

				bool result = model.AddToModel(insertModel.DocumentElement, true);

				Assertion.Assert("Insert returned false.", result);
				
				XmlDocument resultModel = new XmlDocument();
				
				resultModel.LoadXml(
					"<page>" +
					" <title>Page Title</title>" +
					" <contents>" +
					"  <div>" +
					"   Hello World!" +
					"  </div>" +
					" </contents>" +
					" <identity>" +
					"  aaron" +
					" </identity>" +
					"</page>");
				
				Assertion.Assert("New model was not correctly inserted.  Model is " +
					model.m_modelDom.OuterXml + "\nShould have been " + resultModel.OuterXml, 
					model.m_modelDom.OuterXml == resultModel.OuterXml);
			}

			[Test]
			public void GetModelString()
			{
				XmlAggregatorModel model = new XmlAggregatorModel(m_inModel);

				Assertion.Assert("Input model doesn't match document model.\n" 
					+ "Original = " + m_inModel.OuterXml + "\n" 
					+ "Document Model = " +	model.GetModel().OuterXml, 
					model.GetModel().OuterXml == m_inModel.OuterXml);
			}
		}

		#endif

		#endregion	
	}
}
